﻿#region --- License & Copyright Notice
/*
CRUD Services Library

Copyright 2012 Jeevan James

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Entity;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;

namespace CrudServices.EF
{
    public class CrudServiceEF : ICrudService
    {
        private readonly Func<DbContext> _contextFactory;
        private readonly Func<string, Type> _entityResolver;
        private string _contextFormat;

        public CrudServiceEF(Func<DbContext> contextFactory, Func<string, Type> entityResolver = null)
        {
            if (contextFactory == null)
                throw new ArgumentNullException("contextFactory");

            _contextFactory = contextFactory;
            _entityResolver = entityResolver ?? DefaultEntityResolver;

            BuildCaches();
        }

        private Type DefaultEntityResolver(string entityName)
        {
            if (_contextFormat == null)
            {
                using (DbContext context = _contextFactory())
                {
                    Type contextType = context.GetType();
                    _contextFormat = string.Format("{0}.{{0}}, {1}", contextType.Namespace, contextType.Assembly.FullName);
                }
            }
            return Type.GetType(string.Format(_contextFormat, entityName), false, false);
        }

        public string Search(SearchCriteria criteria)
        {
            EntityInfo entityInfo;
            if (!_entityNameInfoMapping.TryGetValue(criteria.Source, out entityInfo))
                return null;

            using (DbContext context = _contextFactory())
            {
                object data = entityInfo.SetProperty.GetValue(context);
                data = DoWhere(data, entityInfo, criteria.WhereCriteria);
                data = DoSort(data, entityInfo, criteria.SortFields);
                data = DoProject(data, entityInfo, criteria.ProjectionTarget);
                var enumerable = (IEnumerable)data;
                IList results;
                if (criteria.ProjectionTarget == null)
                    results = (IList)Activator.CreateInstance(entityInfo.GenericListType);
                else
                    results = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(_entityResolver(criteria.ProjectionTarget)));
                foreach (object entity in enumerable)
                    results.Add(entity);

                string json = Serialize(results);
                return json;
            }
        }

        private object DoProject(object queryable, EntityInfo entityInfo, string projectionTarget)
        {
            if (projectionTarget == null)
                return queryable;

            Type projectionType = _entityResolver(projectionTarget);
            if (projectionType == null)
                throw new Exception("Cannot find type for projection " + projectionTarget);

            PropertyInfo[] entityProperties = entityInfo.Type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            PropertyInfo[] projectionProperties = projectionType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(prop => entityProperties.Any(ep => ep.Name == prop.Name)).ToArray();

            MethodInfo selectMethod = _queryableMethods[SelectMethodIndex];
            selectMethod = selectMethod.MakeGenericMethod(entityInfo.Type, projectionType);

            ParameterExpression x = Expression.Variable(entityInfo.Type, "x");
            var propertyAssignments = new MemberBinding[projectionProperties.Length];
            for (int i = 0; i < projectionProperties.Length; i++)
            {
                PropertyInfo entityProperty = entityProperties.Single(prop => prop.Name == projectionProperties[i].Name);
                propertyAssignments[i] = Expression.Bind(projectionProperties[i], Expression.Property(x, entityProperty));
            }
            MemberInitExpression ctorCall = Expression.MemberInit(Expression.New(projectionType), propertyAssignments);
            LambdaExpression lambda = Expression.Lambda(ctorCall, x);

            object result = selectMethod.Invoke(null, new[] { queryable, lambda });
            return result;
        }

        private object DoSort(object queryable, EntityInfo entityInfo, IList<SortCriterion> criteria)
        {
            if (criteria == null || criteria.Count == 0)
                return queryable;

            object result = queryable;

            foreach (SortCriterion sortCriterion in criteria)
            {
                Type sortKeyType = entityInfo.Type.GetProperty(sortCriterion.FieldName).PropertyType;
                MethodInfo sortMethodTemplate = _queryableMethods[sortCriterion.Order == SortOrder.Ascending ? OrderByMethodIndex : OrderByDescMethodIndex];
                MethodInfo orderByMethod = sortMethodTemplate.MakeGenericMethod(entityInfo.Type, sortKeyType);
                ParameterExpression x = Expression.Variable(entityInfo.Type, "x");
                MemberExpression property = Expression.Property(x, sortCriterion.FieldName);
                LambdaExpression lambda = Expression.Lambda(property, x);

                result = orderByMethod.Invoke(null, new[] { result, lambda });
            }
            return result;
        }

        private object DoWhere(object queryable, EntityInfo entityInfo, WhereCriteria criteria)
        {
            if (criteria == null || criteria.Count == 0)
                return queryable;

            return queryable;
        }

        public string Find(string entityName, int id)
        {
            Type entityType = ResolveEntityType(entityName);
            using (DbContext context = _contextFactory())
            {
                DbSet set = context.Set(entityType);
                object item = set.Find(id);
                return Serialize(item);
            }
        }

        public string Add(string entityName, string items)
        {
            Type entityType = ResolveEntityType(entityName);
            var itemsToAdd = (IEnumerable)Deserialize(typeof(IEnumerable<>).MakeGenericType(entityType), items);
            using (DbContext context = _contextFactory())
            {
                DbSet set = context.Set(entityType);
                using (var transaction = new TransactionScope())
                {
                    foreach (object item in itemsToAdd)
                        set.Add(item);
                    context.SaveChanges();
                    transaction.Complete();
                }
            }
            return null;
        }

        public int Update(string items)
        {
            throw new NotImplementedException();
        }

        public int Delete(string items)
        {
            throw new NotImplementedException();
        }

        public int DeleteIds(string name, IEnumerable<int> itemIds)
        {
            throw new NotImplementedException();
        }

        public string SaveChanges(string items)
        {
            throw new NotImplementedException();
        }

        private Type ResolveEntityType(string entityName)
        {
            Type entityType = _entityResolver(entityName);
            if (entityType == null)
                throw new Exception("Cannot resolve entity with name " + entityName);
            return entityType;
        }

        private static string Serialize(object value)
        {
            if (value == null)
                return null;
            var serializer = new DataContractJsonSerializer(value.GetType());
            using (var stream = new MemoryStream())
            {
                serializer.WriteObject(stream, value);
                byte[] array = stream.ToArray();
                return Encoding.UTF8.GetString(array, 0, array.Length);
            }
        }

        private static object Deserialize(Type type, string json)
        {
            var deserializer = new DataContractJsonSerializer(type);
            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
                return deserializer.ReadObject(stream);
        }

        private readonly Dictionary<string, EntityInfo> _entityNameInfoMapping = new Dictionary<string,EntityInfo>();
        private readonly List<MethodInfo> _queryableMethods = new List<MethodInfo>(4);

        private const int SelectMethodIndex = 0;
        private const int OrderByMethodIndex = 1;
        private const int OrderByDescMethodIndex = 2;
        private const int WhereMethodIndex = 3;

        private void BuildCaches()
        {
            Task.WaitAll(Task.Factory.StartNew(BuildEntityNameDbSetMapping), Task.Factory.StartNew(CacheQueryableMethods));
        }

        private void BuildEntityNameDbSetMapping()
        {
            using (DbContext context = _contextFactory())
            {
                PropertyInfo[] properties = context.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
                foreach (PropertyInfo property in properties)
                {
                    if (property.GetIndexParameters().Length > 0)
                        continue;
                    Type propertyType = property.PropertyType;
                    if (!propertyType.IsGenericType)
                        continue;
                    if (propertyType.GetGenericTypeDefinition() != typeof(DbSet<>))
                        continue;
                    Type entityType = propertyType.GetGenericArguments()[0];
                    _entityNameInfoMapping.Add(entityType.Name,
                        new EntityInfo(property, entityType, typeof(List<>).MakeGenericType(entityType)));
                }
            }
        }

        private void CacheQueryableMethods()
        {
            MethodInfo[] queryableMethods = typeof(Queryable).GetMethods(BindingFlags.Public | BindingFlags.Static);

            MethodInfo selectMethod = Array.Find(queryableMethods, mi => {
                if (mi.Name != "Select")
                    return false;
                ParameterInfo[] parameters = mi.GetParameters();
                if (parameters.Length != 2)
                    return false;
                if (!parameters[1].ParameterType.IsGenericType)
                    return false;
                Type[] genericArguments = parameters[1].ParameterType.GetGenericArguments();
                if (genericArguments.Length != 1 || genericArguments[0].GetGenericTypeDefinition() != typeof(Func<,>))
                    return false;
                return true;
            });
            _queryableMethods.Add(selectMethod);

            var orderByMethod = Array.Find(queryableMethods, mi => mi.Name == "OrderBy" && mi.GetParameters().Length == 2);
            _queryableMethods.Add(orderByMethod);

            var orderByDescMethod = Array.Find(queryableMethods, mi => mi.Name == "ThenByDescending" && mi.GetParameters().Length == 2);
            _queryableMethods.Add(orderByDescMethod);

            MethodInfo whereMethod = Array.Find(queryableMethods, mi => {
                if (mi.Name != "Where")
                    return false;
                ParameterInfo[] parameters = mi.GetParameters();
                if (parameters.Length != 2)
                    return false;
                if (!parameters[1].ParameterType.IsGenericType)
                    return false;
                Type[] genericArguments = parameters[1].ParameterType.GetGenericArguments();
                if (genericArguments.Length != 1 || genericArguments[0].GetGenericTypeDefinition() != typeof(Func<,>))
                    return false;
                return true;
            });
            _queryableMethods.Add(whereMethod);
        }

        private sealed class EntityInfo
        {
            private readonly PropertyInfo _setProperty;
            private readonly Type _type;
            private readonly Type _genericListType;

            internal EntityInfo(PropertyInfo setProperty, Type type, Type genericListType)
            {
                _setProperty = setProperty;
                _type = type;
                _genericListType = genericListType;
            }

            internal PropertyInfo SetProperty
            {
                get { return _setProperty; }
            }

            internal Type Type
            {
                get { return _type; }
            }

            internal Type GenericListType
            {
                get { return _genericListType; }
            }
        }
    }
}